﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using log4net;
using Lucene.Net.Search;
using Lucene.Net.Index;
using Lucene.Net.Analysis.Standard;
using Lucene.Net.Documents;

namespace MyLib.Data {
	/// <summary>
	/// 
	/// </summary>
	public class FulltextProvider : FileContextProvider {
		/// <summary>
		/// 
		/// </summary>
		private sealed class FulltextTableEntry {
			/// <summary>
			/// 
			/// </summary>
			public int SourceType { get; private set; }

			/// <summary>
			/// 
			/// </summary>
			public string IdColumn { get; private set; }

			/// <summary>
			/// 
			/// </summary>
			public string TitleColumn { get; private set; }

			/// <summary>
			/// 
			/// </summary>
			public string ContentColumn { get; private set; }

			/// <summary>
			/// 
			/// </summary>
			/// <param name="sourceType"></param>
			/// <param name="idColumn"></param>
			/// <param name="titleColumn"></param>
			/// <param name="contentColumn"></param>
			public FulltextTableEntry(int sourceType, string idColumn, string titleColumn, string contentColumn) {
				this.SourceType = sourceType;
				this.IdColumn = idColumn;
				this.TitleColumn = titleColumn;
				this.ContentColumn = contentColumn;
			}

			/// <summary>
			/// 
			/// </summary>
			/// <param name="obj"></param>
			/// <returns></returns>
			public override bool Equals(object obj) {
				var ano = obj as FulltextTableEntry;

				if (ano == null) {
					return false;
				} else {
					return ano.SourceType == this.SourceType;
				}
			}

			/// <summary>
			/// 
			/// </summary>
			/// <returns></returns>
			public override int GetHashCode() {
				return SourceType;
			}

			/// <summary>
			/// 
			/// </summary>
			/// <returns></returns>
			public override string ToString() {
				return string.Format("Source type = {0}, Id column = \"{1}\", Title column = \"{2}\" Content column = \"{3}\"", SourceType, IdColumn, TitleColumn, ContentColumn);
			}
		} // end of FulltextTableEntry.

		private const int TopSearchResult = 10000;

		private const int DefaultMaximumRows = 10;

		private static IList<FulltextTableEntry> m_tableEntries = new List<FulltextTableEntry>();

		private ILog m_logger = LogManager.GetLogger(typeof(FulltextProvider));

		private string m_indexFilePath;

		/// <summary>
		/// 
		/// </summary>
		public int CacheTimeout { get; set; }

		/// <summary>
		/// 
		/// </summary>
		/// <param name="orderColumns"></param>
		/// <param name="keywords"></param>
		/// <param name="maximumRows"></param>
		/// <param name="startRowIndex"></param>
		/// <returns></returns>
		public FulltextIndexTable Find(string keywords, string orderColumns = null, int? maximumRows = null, int? startRowIndex = null) {
			var dt = new FulltextIndexTable();

			if (string.IsNullOrWhiteSpace(keywords)) {
				return dt;
			}

			if (string.IsNullOrEmpty(orderColumns)) {
				orderColumns = "weight desc";
			}

			// Create query.
			var query = CreateQueryByKeywords(keywords);

			// Execute query.
			var sd = Lucene.Net.Store.FSDirectory.Open(m_indexFilePath);

			if (!Lucene.Net.Index.IndexReader.IndexExists(sd)) {
				sd.Dispose();
				return dt;
			}

			using (sd)
			using (var searcher = new IndexSearcher(sd)) {

				var ret = searcher.Search(query, TopSearchResult);

				if (ret != null) {
					var sdocs = ret.ScoreDocs;

					if (sdocs != null && sdocs.Length != 0) {
						var startIndex = startRowIndex ?? 0;
						var endIndex = sdocs.Length;
						if (maximumRows.HasValue && (startIndex < sdocs.Length - maximumRows.Value)) {
							endIndex = startIndex + maximumRows.Value + 1;
						}

						for (int i = startRowIndex ?? 0; i < endIndex; ++i) {
							var sdoc = sdocs[i];

							var dd = searcher.Doc(sdoc.Doc);

							var nrow = dt.AddNewRow();

							nrow.SourceId = dd.Get("source_id");
							nrow.SourceSubId = dd.Get("source_sub_id");
							nrow.SourceType = int.Parse(dd.Get("source_type"));
							nrow.Title = dd.Get("title");
							nrow.Description = dd.Get("description");
							nrow.Score = (int)sdoc.Score;
						} // end of for.

						dt.AcceptChanges();
					} // end of if.
				} // end of if.

			}

			query = null;

			GC.Collect(2, GCCollectionMode.Optimized);

			return dt;
		} // end of Find.

		/// <summary>
		/// 
		/// </summary>
		/// <param name="keywords"></param>
		/// <returns></returns>
		public int FindCount(string keywords) {
			if (string.IsNullOrWhiteSpace(keywords)) {
				return 0;
			}

			// Create query.
			var query = CreateQueryByKeywords(keywords);

			// Execute query.
			var sd = Lucene.Net.Store.FSDirectory.Open(m_indexFilePath);

			if (!Lucene.Net.Index.IndexReader.IndexExists(sd)) {
				sd.Dispose();
				return 0;
			}

			var cnt = 0;

			using (sd)
			using (var searcher = new IndexSearcher(sd)) {

				var ret = searcher.Search(query, TopSearchResult);

				if (ret != null) {
					var sdocs = ret.ScoreDocs;

					cnt = sdocs == null ? 0 : sdocs.Length;
				} // end of if.
			}

			query = null;

			GC.Collect(2, GCCollectionMode.Optimized);

			return cnt;
		} // end of Find.

		/// <summary>
		/// 
		/// </summary>
		/// <param name="keywords"></param>
		/// <returns></returns>
		private static BooleanQuery CreateQueryByKeywords(string keywords) {
			var query = new BooleanQuery();
			foreach (var kw in keywords.Split(' ', ',', ';')) {
				if (string.IsNullOrWhiteSpace(kw)) {
					continue;
				}

				var cbq = new BooleanQuery();
				var titleQuery = new TermQuery(new Term("title", kw));
				var descQuery = new TermQuery(new Term("description", kw));
				var contentQuery = new TermQuery(new Term("content", kw));

				// Adjust boost.
				titleQuery.Boost = 10.0F;
				descQuery.Boost = 5.0F;
				contentQuery.Boost = 1.0F;

				cbq.Add(new BooleanClause(titleQuery, Occur.SHOULD));
				cbq.Add(new BooleanClause(descQuery, Occur.SHOULD));
				cbq.Add(new BooleanClause(contentQuery, Occur.SHOULD));

				query.Add(cbq, Occur.MUST);
			}

			return query;
		} // end of CreateQueryByKeywords.

		/// <summary>
		/// 
		/// </summary>
		/// <param name="sourceType"></param>
		/// <param name="id"></param>
		/// <param name="subId"></param>
		/// <param name="title"></param>
		/// <param name="description"></param>
		/// <param name="contents"></param>
		/// <returns></returns>
		public int Save(int sourceType, string id, string subId, string title, string description, IEnumerable<string> contents) {
			System.Diagnostics.Debug.Assert(sourceType != 0);
			System.Diagnostics.Debug.Assert(!string.IsNullOrWhiteSpace(id));

			if (subId == null) {
				subId = "";
			}

			if (title == null) {
				title = "";
			}

			title = title.Trim();

			if (title.Length == 0) {
				return 0;
			}

			if (description == null) {
				description = "";
			}

			var sd = Lucene.Net.Store.FSDirectory.Open(m_indexFilePath);

			var analyzer = new Lucene.Net.Analysis.PanGu.PanGuAnalyzer();

			bool iscreate = !Lucene.Net.Index.IndexReader.IndexExists(sd);

			using (sd)
			using (analyzer)
			using (var writer = new IndexWriter(sd, analyzer, iscreate, IndexWriter.MaxFieldLength.UNLIMITED)) {
				var query = new BooleanQuery();
				query.Add(new BooleanClause(new TermQuery(new Term("source_type", sourceType.ToString())), Occur.MUST));
				query.Add(new BooleanClause(new TermQuery(new Term("source_id", id)), Occur.MUST));
				query.Add(new BooleanClause(new TermQuery(new Term("source_sub_id", subId)), Occur.MUST));
				writer.DeleteDocuments(query);

				var doc = new Document();

				doc.Add(new Field("source_type", sourceType.ToString(), Field.Store.YES, Field.Index.NOT_ANALYZED));
				doc.Add(new Field("source_id", id, Field.Store.YES, Field.Index.NOT_ANALYZED));
				doc.Add(new Field("source_sub_id", subId, Field.Store.YES, Field.Index.NOT_ANALYZED));
				doc.Add(new Field("title", title, Field.Store.YES, Field.Index.ANALYZED));
				doc.Add(new Field("description", description, Field.Store.YES, Field.Index.ANALYZED));

				if (contents != null) {
					foreach (var content in contents) {
						if (string.IsNullOrWhiteSpace(content)) {
							continue;
						}

						doc.Add(new Field("content", content, Field.Store.NO, Field.Index.ANALYZED));
					}
				}

				writer.Optimize();

				writer.AddDocument(doc);
			}

			return 1;
		} // end of Save.

		/// <summary>
		/// 
		/// </summary>
		/// <param name="sourceType"></param>
		/// <param name="id"></param>
		/// <param name="subId"></param>
		/// <returns></returns>
		public int Delete(int sourceType, string id, string subId) {
			System.Diagnostics.Debug.Assert(sourceType != 0);
			System.Diagnostics.Debug.Assert(!string.IsNullOrWhiteSpace(id));

			if (subId == null) {
				subId = "";
			}

			var sd = Lucene.Net.Store.FSDirectory.Open(m_indexFilePath);

			var analyzer = new Lucene.Net.Analysis.PanGu.PanGuAnalyzer();

			if (!Lucene.Net.Index.IndexReader.IndexExists(sd)) {
				return 0;
			}

			using (sd)
			using (analyzer)
			using (var writer = new IndexWriter(sd, analyzer, false, IndexWriter.MaxFieldLength.UNLIMITED)) {
				var query = new BooleanQuery();
				query.Add(new BooleanClause(new TermQuery(new Term("source_type", sourceType.ToString())), Occur.MUST));
				query.Add(new BooleanClause(new TermQuery(new Term("source_id", id)), Occur.MUST));
				query.Add(new BooleanClause(new TermQuery(new Term("source_sub_id", subId)), Occur.MUST));
				writer.DeleteDocuments(query);
			}

			return 1;
		} // end of Delete.

		/// <summary>
		/// 
		/// </summary>
		public void Clear() {
			var sd = Lucene.Net.Store.FSDirectory.Open(m_indexFilePath);

			var analyzer = new StandardAnalyzer(Lucene.Net.Util.Version.LUCENE_30);

			using (sd)
			using (var writer = new IndexWriter(sd, analyzer, true, IndexWriter.MaxFieldLength.UNLIMITED)) {
				writer.DeleteAll();
				writer.Commit();
			}
		} // end of Clear.

		/// <summary>
		/// 
		/// </summary>
		/// <param name="sourceType"></param>
		/// <param name="idColumn"></param>
		/// <param name="titleColumn"></param>
		/// <param name="contentColumn"></param>
		public static void RegisterSource(int sourceType, string idColumn, string titleColumn, string contentColumn) {
			var entry = new FulltextTableEntry(sourceType, idColumn, titleColumn, contentColumn);

			if (m_tableEntries.Contains(entry)) {
				throw new ArgumentException(
					string.Format("Source type \"{0}\" already exists.", entry.SourceType));
			}

			m_tableEntries.Add(entry);
		} // end of RegisterSource.

		/// <summary>
		/// 
		/// </summary>
		public FulltextProvider()
			: base() {
			this.m_indexFilePath = System.Configuration.ConfigurationManager.AppSettings["MyLib.FulltextProvider.IndexFilePath"];

			this.CacheTimeout = 600;
		}
	} // end of FulltextProvider.
}
